﻿using SuperDump.Analyzer.Linux.Boundary;
using SuperDump.Common;
using SuperDump.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace SuperDump.Analyzer.Linux.Analysis {
	public class DebugSymbolResolver {

		private readonly IFilesystem filesystem;
		private readonly IHttpRequestHandler requestHandler;
		private readonly IProcessHandler processHandler;

		public DebugSymbolResolver(IFilesystem filesystem, IHttpRequestHandler requestHandler, IProcessHandler processHandler) {
			this.filesystem = filesystem ?? throw new ArgumentNullException("Filesystem Helper must not be null!");
			this.requestHandler = requestHandler ?? throw new ArgumentNullException("RequestHandler must not be null!");
			this.processHandler = processHandler ?? throw new ArgumentNullException("ProcessHandler must not be null!");
		}

		public void Resolve(IList<SDModule> libs) {
			DownloadDebugSymbolsAsync(WithoutDuplicates(libs)).Wait();
		}

		private IEnumerable<SDModule> WithoutDuplicates(IList<SDModule> libs) {
			return libs
				.GroupBy(l => l.FilePath)
				.Select(g => g.First());
		}

		private async Task DownloadDebugSymbolsAsync(IEnumerable<SDModule> modules) {
			foreach (SDModule module in modules) {
				try {
					if (module is SDCDModule) {
						await DownloadDebugSymbolForModuleAsync((SDCDModule)module);
					} else {
						throw new InvalidCastException("Provided module is not a coredump module!");
					}
				} catch (Exception e) {
					Console.WriteLine($"Failed to resolve debug symbols for {module.FileName}: {e.Message}");
				}
			}
		}

		private async Task DownloadDebugSymbolForModuleAsync(SDCDModule module) {
			if (module.LocalPath != null && IsDynatraceModule(module)) {
				string hash = filesystem.Md5FromFile(module.LocalPath);
				if (IsDebugFileAvailable(module, hash)) {
					module.DebugSymbolPath = Path.GetFullPath(DebugFilePath(module.LocalPath, hash));
				} else {
					await DownloadDebugSymbolsAsync(module, hash);
				}

				await UnstripLibrary(module, hash);
			}
		}

		/// <summary>
		/// Overrides the original *.so with the unstripped binary
		/// </summary>
		private async Task UnstripLibrary(SDCDModule module, string hash) {
			if (IsDebugFileAvailable(module, hash)) {
				string tempBinary = module.LocalPath + ".old";
				if (filesystem.GetFile(tempBinary).Exists) {
					filesystem.Delete(tempBinary);
				}
				filesystem.Move(module.LocalPath, tempBinary);
				await processHandler.ExecuteProcessAndGetOutputAsync("eu-unstrip",
					$"-o {module.LocalPath} {tempBinary} {DebugFilePath(module.LocalPath, hash)}");
				filesystem.Delete(tempBinary);
			}
		}

		private bool IsDynatraceModule(SDCDModule module) {
			if (module.FileName.Contains("libruxit") || module.FileName.Contains("liboneagent")) {
				return true;
			}
			if (module.FilePath.Contains("/lib/ruxit") || module.FilePath.Contains("/lib64/ruxit") ||
				module.FilePath.Contains("/lib/oneagent") || module.FilePath.Contains("/lib64/oneagent") ||
			    module.FilePath.Contains("/lib/dtzagent") || module.FilePath.Contains("/lib64/dtzagent")) {
				return true;
			}
			return false;
		}

		private bool IsDebugFileAvailable(SDCDModule module, string hash) {
			return filesystem.GetFile(DebugFilePath(module.LocalPath, hash)).Exists;
		}

		private async Task DownloadDebugSymbolsAsync(SDCDModule lib, string hash) {
			Console.WriteLine($"Trying to retrieve debug symbols for {lib.FilePath}");
			if (string.IsNullOrEmpty(Configuration.DEBUG_SYMBOL_URL_PATTERN)) {
				Console.WriteLine("Debug symbol URL pattern is not set. No debug symbols will be downloaded.");
				return;
			}
			string url = Configuration.DEBUG_SYMBOL_URL_PATTERN.Replace("{hash}", hash).Replace("{file}", DebugFileName(lib.LocalPath));

			string localDebugFile = DebugFilePath(lib.LocalPath, hash);
			try {
				if (await requestHandler.DownloadFromUrlAsync(url, localDebugFile)) {
					Console.WriteLine($"Successfully downloaded debug symbols for {lib.FilePath}. Stored at {localDebugFile}");
					lib.DebugSymbolPath = Path.GetFullPath(localDebugFile);
				} else {
					Console.WriteLine($"Failed to download debug symbols for {lib.FilePath}. URL: {url}");
				}
			} catch (Exception e) {
				Console.WriteLine($"Failed to download debug symbol: {e.ToString()}");
			}
		}

		private string DebugFilePath(string path, string hash) {
			return Path.Combine(Configuration.DEBUG_SYMBOL_PATH, hash, DebugFileName(path));
		}

		public static string DebugFileName(string path) {
			return $"{(Path.GetExtension(path).Equals(".node") ? "lib" : "")}{Path.GetFileNameWithoutExtension(path)}.dbg";
		}
	}
}
